package time.compiler;
import haxe.macro.MacroTools;
import time.compiler.query.Data;
import time.providers.base.compiler.Generator;
import haxe.macro.Expr;
import time.compiler.query.Solver;
import time.providers.Provider;

#if macro
private typedef MContext = haxe.macro.Context;
#end

/**
 * A compiler context is the compiler-side extension of the Provider object.
 * It contains all provider-specific information that should affect the compiler,
 * such as the Solvers used 
 * 
 * @author waneck
 */

class Context 
{
	private static var providers:Hash<Context> = new Hash();
	private static var byLikeness:Hash<Context> = new Hash();
	
	public static function get(p:Provider):Context
	{
		var ctx = providers.get(p.sourcePath);
		if (null != ctx)
			return ctx;
		
		//search by likeness
		var likeness = p.getLikeness();
		ctx = byLikeness.get(likeness);
		if (null != ctx)
		{
			providers.set(p.sourcePath, ctx);
			return ctx;
		}
		
		var c = new Context();
		c.init(p);
		
		providers.set(p.sourcePath, c);
		byLikeness.set(likeness, c);
		
		return c;
	}
	
	public var solvers(default, null):SolverContainer;
	private var gen:Generator;
	
	public function new() 
	{
		solvers = new SolverContainer(new Solver());
	}
	
	//TODO make an universal way to do warning
	public static dynamic function warning(str:String, pos:Null<Position>) : Dynamic
	{
		#if macro
		if (null == pos)
			pos = MContext.currentPos();
		MContext.warning(str, pos);
		#else
		
		trace("WARNING: " + str + " @  " + pos);
		#end
		
		return null;
	}
	
	public static dynamic function error(str:String, pos:Null<Position>) : Dynamic
	{
		#if macro
		if (null == pos)
			pos = MContext.currentPos();
		MContext.error(str, pos);
		#else
		
		throw str;
		#end
		return null;
	}
	
	public static function getProvider(cls:String, field:String, pos:Position) : Null<Provider>
	{
		var cls = Type.resolveClass(cls);
		if (null == cls)
			return null; //no warning since it might exist but not be compiled (unused)
		if (!Reflect.hasField(cls, field))
			warning(cls + "." + field + " does not exist.", pos);
		
		//TODO proper error for null provider; cast will throw error
		return cast(Reflect.field(cls, field), Provider);
	}
	
	
	public static function parseQuery(queryExpr:Expr, ?providers:Array<Expr>) : Array<{query:ParsedQuery, providerLikeness:String}>
	{
		var ps:Hash<Provider> = new Hash();
		
		if (null == providers || providers.length == 0)
		{
			var len = 0;
			for (alias in Provider.getFromAlias("Default"))
			{
				ps.set(alias.getLikeness(), alias);
				len++;
			}
			
			if (len == 0)
				error("No provider is defined, and there is no provider with alias Default set.", queryExpr.pos);
		} else {
			for (p in providers)
			{
				switch(p.expr)
				{
					case EConst(c): switch(c) {
						case CString(s):
							for (p in Provider.getFromAlias(s))
								ps.set(p.getLikeness(), p);
						#if macro //meaning: if not macro, we cannot use this short form for current class
						case CType(s), CIdent(s):
							var cls = MContext.getLocalClass();
							
							var statics = cls.get().statics.get();
							if (!Lambda.exists(statics, function(f) return f.name == s))
							{
								error("Only paths to static Provider variables or string aliases can be passed as argument", p.pos);
							}
							
							var c = getProvider(cls.toString(), s, p.pos);
							if (null != c)
							{
								ps.set(c.getLikeness(), c);
							}
						#end
						default: error("Only paths to static Provider variables or string aliases can be passed as argument", p.pos);
					}
					
					case EType(e1, f), EField(e1, f):
						var cpath = 
						#if macro
							switch(MContext.typeof(e1))
							{
								case TType(t, _):
									var t = t.toString();
									if (t.charCodeAt(0) == "#".code)
									{
										t.substr(1);
									} else {
										error("Only paths to static Provider variables or string aliases must be passed as argument", p.pos);
									}
								default:
									error("Only paths to static Provider variables or string aliases must be passed as argument", p.pos);
							};
						#else
							MacroTools.getPath(e1).join(".");
						#end
						var path = cpath + "." + f;
						var c = getProvider(cpath, f, p.pos);
						if (null != c)
						{
							ps.set(c.getLikeness(), c);
						}
					default: error("Only paths to static Provider variables or string aliases must be passed as argument", p.pos);
				}
			}
		}
		
		//TODO make logic to determine which solvers were used to parse the query
		//this way we can join different providers if they don't use their specific
		//solvers OPTIMIZATION HIGH
		var ret = [];
		for (p in ps)
		{
			var ctx = get(p);
			var parser = new time.compiler.query.Parser(ctx);
			ret.push({query:parser.parse(queryExpr), providerLikeness:p.getLikeness()});
		}
		
		return ret;
	}
	
	
	private function init(p:Provider)
	{
		this.gen = cast(Type.createInstance(p.driver.getGeneratorClass(), []), Generator);
		this.gen.register(solvers);
		
		var exts = p.getExtensions();
		for (ext in exts)
		{
			var cl:Class<Solver> = cast Type.resolveClass(ext);
			if (null == cl)
			{
				warning("Required extension " + ext + " not found", null);
				continue;
			}
			
			var t = Type.createInstance(cl, []);
			t.register(solvers);
		}
	}
}